package cc.aliza.lib.mongo.core;

import cc.aliza.lib.mongo.core.cache.FieldsCache;
import cc.aliza.lib.mongo.core.utils.IdUtil;
import cc.aliza.lib.mongo.core.utils.MapperUtil;
import cc.aliza.lib.mongo.core.utils.Operator;
import cc.aliza.lib.mongo.core.utils.ReferenceUtil;
import com.mongodb.BasicDBObject;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.model.FindOneAndUpdateOptions;
import com.mongodb.client.model.ReturnDocument;
import com.mongodb.client.model.UpdateOptions;
import org.bson.Document;
import org.bson.types.ObjectId;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
 * 『给时光以生命·给岁月以文明』
 *
 * @author Zhang Jing (byrdkm17@gmail.com).
 */
@SuppressWarnings("unchecked")
public class AlizaUpdater<T> {

    private final Document modifier = new Document();
    private AlizaDao<T> dao;

    public AlizaUpdater(AlizaDao<T> dao) {
        this.dao = dao;
    }

    private Object checkValue(String key, Object value) {
        Class<T> clazz = dao.getEntityClass();
        Object result = value;
        if (value instanceof AlizaEntity) {
            result = ReferenceUtil.toDBRef((AlizaEntity) value);
        } else if (!(value instanceof Document) && FieldsCache.getInstance().isEmbed(clazz, key)) {
            result = MapperUtil.toDocument(value);
        }
        return result;
    }

    private void append(String op, String key, Object value) {
        Object obj = modifier.get(op);
        Document document;
        if (!(obj instanceof Document)) {
            document = new Document(key, value);
            modifier.put(op, document);
        } else {
            document = (Document) modifier.get(op);
            document.put(key, value);
        }
    }

    public void apply(T t) {
        apply(((AlizaEntity) t).getId());
    }

    public void apply(String id) {
        Class<T> clazz = dao.getEntityClass();
        Document condition = new Document(Operator.ID, IdUtil.toDbId(clazz, id));
        set("updateTime", new Date());
        dao.getCollection().withWriteConcern(dao.getConcern()).updateOne(condition, modifier, new UpdateOptions().upsert(false));
        if (dao.getListener() != null) {
            dao.notifyUpdated((AlizaEntity) dao.findOne(id));
        }
    }

    public void apply() {
        apply(new Document());
    }

    public void apply(String key, Object value) {
        apply(new Document(key, value));
    }

    public void apply(AlizaQuery query) {
        apply(query.getCondition());
    }

    public void apply(Document condition) {
        List ids = null;
        if (dao.getListener() != null) {
            ids = dao.getCollection().distinct(Operator.ID, ObjectId.class).filter(condition).into(new ArrayList<>());
        }
        set("updateTime", new Date());
        dao.getCollection().withWriteConcern(dao.getConcern()).updateMany(condition, modifier, new UpdateOptions().upsert(false));
        if (ids != null) {
            Document in = new Document(Operator.IN, ids);
            MongoCursor<Document> cursor = dao.getCollection().find(new Document(Operator.ID, in)).iterator();
            List<T> list = MapperUtil.toList(dao.getEntityClass(), cursor);
            list.stream().forEach(o -> dao.notifyUpdated((AlizaEntity) o));
        }
    }

    public T findOneAndSet(T t, String key, Object value) {
        return findOneAndSet(dao.query().is("_id", ((AlizaEntity) t).getId()), key, value);
    }

    public T findOneAndSet(AlizaQuery query, String key, Object value) {
        return findOneAndSet(query, key, value, options());
    }

    public T findOneAndSet(AlizaQuery query, String key, Object value, FindOneAndUpdateOptions options) {
        return findOneAndUpdate(query.getCondition(), new Document(Operator.SET, new Document(key, value)), options);
    }

    public T findOneAndSet(AlizaQuery query, Map<String, Object> values) {
        return findOneAndSet(query, values, options());
    }

    public T findOneAndSet(String id, Map<String, Object> values) {
        return findOneAndSet(dao.query().is("_id", id), values, options());
    }

    public T findOneAndSet(AlizaQuery query, Map<String, Object> values, FindOneAndUpdateOptions options) {
        Document update = new Document();
        values.forEach(update::put);
        return findOneAndUpdate(query.getCondition(), new Document(Operator.SET, update), options);
    }

    public T findOneAndInc(T t, String key, long value) {
        return findOneAndInc(dao.query().is("_id", ((AlizaEntity) t).getId()), key, value);
    }

    public T findOneAndInc(AlizaQuery query, String key, long value) {
        return findOneAndInc(query, key, value, options());
    }

    public T findOneAndInc(AlizaQuery query, String key, long value, FindOneAndUpdateOptions options) {
        return findOneAndUpdate(query.getCondition(), new Document(Operator.INC, new Document(key, value)), options);
    }

    public T findOneAndInc(T t, String key, int value) {
        return findOneAndInc(dao.query().is("_id", ((AlizaEntity) t).getId()), key, value);
    }

    public T findOneAndInc(AlizaQuery query, String key, int value) {
        return findOneAndInc(query, key, value, options());
    }

    public T findOneAndInc(AlizaQuery query, String key, int value, FindOneAndUpdateOptions options) {
        return findOneAndUpdate(query.getCondition(), new Document(Operator.INC, new Document(key, value)), options);
    }

    public T findOneAndInc(AlizaQuery query, Map<String, Integer> values) {
        return findOneAndInc(query, values, options());
    }

    public T findOneAndInc(AlizaQuery query, Map<String, Integer> values, FindOneAndUpdateOptions options) {
        Document update = new Document();
        values.forEach(update::put);
        return findOneAndUpdate(query.getCondition(), new Document(Operator.INC, update), options);
    }

    private FindOneAndUpdateOptions options() {
        return new FindOneAndUpdateOptions().upsert(true).returnDocument(ReturnDocument.AFTER);
    }

    private T findOneAndUpdate(Document filter, Document update, FindOneAndUpdateOptions options) {
        // 设置记录更新时间
        update.get(Operator.SET);
        Object obj = update.get(Operator.SET);
        Document document;
        if (!(obj instanceof Document)) {
            document = new Document("updateTime", new Date());
            update.put(Operator.SET, document);
        } else {
            document = (Document) update.get(Operator.SET);
            document.put("updateTime", new Date());
        }

        return MapperUtil.fromDocument(dao.getEntityClass(), dao.getCollection().findOneAndUpdate(filter, update, options));
    }

    public AlizaUpdater<T> set(String key, Object value) {
        value = checkValue(key, value);
        append(Operator.SET, key, value);
        return this;
    }

    public AlizaUpdater<T> unset(String key) {
        append(Operator.UNSET, key, 1);
        return this;
    }

    public AlizaUpdater<T> inc(String key, Object value) {
        append(Operator.INC, key, value);
        return this;
    }

    public AlizaUpdater<T> mul(String key, Object value) {
        append(Operator.MUL, key, value);
        return this;
    }

    public AlizaUpdater<T> push(String key, Object value) {
        value = checkValue(key, value);
        append(Operator.PUSH, key, value);
        return this;
    }

    public AlizaUpdater<T> pushEach(String key, List valueList) {
        List values = new ArrayList<>();
        valueList.stream().forEach(o -> values.add(checkValue(key, o)));
        append(Operator.PUSH, key, new BasicDBObject(Operator.EACH, values.toArray()));
        return this;
    }

    public AlizaUpdater<T> pull(String key, Object value) {
        value = checkValue(key, value);
        append(Operator.PULL, key, value);
        return this;
    }

    public AlizaUpdater<T> popFirst(String key) {
        append(Operator.POP, key, -1);
        return this;
    }

    public AlizaUpdater<T> popLast(String key) {
        append(Operator.POP, key, 1);
        return this;
    }

    public AlizaUpdater<T> min(String key, Object value) {
        append(Operator.MIN, key, value);
        return this;
    }

    public AlizaUpdater<T> max(String key, Object value) {
        append(Operator.MAX, key, value);
        return this;
    }

    public AlizaUpdater<T> bitwise(String key, int value, Bitwise bitwise) {
        append(Operator.BIT, key, new Document(checkBitwise(bitwise), value));
        return this;
    }

    private String checkBitwise(Bitwise bitwise) {
        String result = null;
        switch (bitwise) {
            case AND:
                result = "and";
                break;
            case OR:
                result = "or";
                break;
            case XOR:
                result = "xor";
                break;
            default:
                break;
        }
        return result;
    }

    public enum Bitwise {AND, OR, XOR}

}
